package models

import (
	"crypto/rand"
	"database/sql"
	"fmt"
	"io"
	"time"

	"beego-api-demo/models/mymysql"

	"github.com/go-sql-driver/mysql"
	"golang.org/x/crypto/scrypt"
)

type User struct {
	ID       string    `json:"id,omitempty"`
	Name     string    `json:"name,omitempty"`
	Password string    `json:"password,omitempty"`
	Salt     string    `json:"salt,omitempty"`
	RegDate  time.Time `json:"reg_date,omitempty"`
}

const pwHashBytes = 64

func generateSalt() (salt string, err error) {
	buf := make([]byte, pwHashBytes)
	if _, err := io.ReadFull(rand.Reader, buf); err != nil {
		return "", err
	}

	return fmt.Sprintf("%x", buf), nil
}

func generatePassHash(password string, salt string) (hash string, err error) {
	h, err := scrypt.Key([]byte(password), []byte(salt), 16384, 8, 1, pwHashBytes)
	if err != nil {
		return "", err
	}

	return fmt.Sprintf("%x", h), nil
}

// NewUser alloc and initialize a user.
func NewUser(r *RegisterForm, t time.Time) (u *User, err error) {
	salt, err := generateSalt()
	if err != nil {
		return nil, err
	}
	hash, err := generatePassHash(r.Password, salt)
	if err != nil {
		return nil, err
	}

	user := User{
		ID:       r.Phone,
		Name:     r.Name,
		Password: hash,
		Salt:     salt,
		RegDate:  t}

	return &user, nil
}

// Insert insert a document to collection.
func (u *User) Insert() (code int, err error) {
	db := mymysql.Conn()

	st, err := db.Prepare("INSERT INTO users(id, name, password, salt, reg_date) VALUES(?, ?, ?, ?, ?)")
	if err != nil {
		return ErrDatabase, err
	}
	defer st.Close()

	if _, err := st.Exec(u.ID, u.Name, u.Password, u.Salt, u.RegDate); err != nil {
		if e, ok := err.(*mysql.MySQLError); ok {
			if e.Number == errNumberDuplicateKey {
				return ErrDupRows, err
			}
			return ErrDatabase, err
		}
		return ErrDatabase, err
	}

	return 0, nil
}

// FindByID query a document according to input id.
func (u *User) FindByID(id string) (code int, err error) {
	db := mymysql.Conn()

	st, err := db.Prepare("SELECT id, name, password, salt, reg_date FROM users WHERE id = ?")
	if err != nil {
		return ErrDatabase, err
	}
	defer st.Close()

	row := st.QueryRow(id)

	var tmpID sql.NullString
	var tmpName sql.NullString
	var tmpPassword sql.NullString
	var tmpSalt sql.NullString
	var tmpRegDate mysql.NullTime
	if err := row.Scan(&tmpID, &tmpName, &tmpPassword, &tmpSalt, &tmpRegDate); err != nil {
		if err == sql.ErrNoRows {
			return ErrNotFound, err
		}
		return ErrDatabase, err
	}

	if tmpID.Valid {
		u.ID = tmpID.String
	}
	if tmpName.Valid {
		u.Name = tmpName.String
	}
	if tmpPassword.Valid {
		u.Password = tmpPassword.String
	}
	if tmpSalt.Valid {
		u.Salt = tmpSalt.String
	}
	if tmpRegDate.Valid {
		u.RegDate = tmpRegDate.Time
	}

	return 0, nil
}

func (u *User) CheckPassword(pass string) (ok bool, err error) {
	hash, err := generatePassHash(pass, u.Salt)
	if err != nil {
		return false, err
	}

	return u.Password == hash, nil
}

func (u *User) ClearPassword() {
	u.Password = ""
	u.Salt = ""
}
